<?php

if (! defined('LIB_PATH'))
{
    die('The file ' . __FILE__ . ' needs to be required from the main executable of PhpPhoenix');
}

require_once LIB_PATH . 'PhpClass.php';
require_once LIB_PATH . 'PhpMethod.php';
require_once LIB_PATH . 'PhpConstant.php';
require_once LIB_PATH . 'PhpVariable.php';

/**
 * Parser 
 *
 * Class that represents the token's parser that groups
 * 
 * @package php-phoenix
 * @subpackage core
 * @version $id$
 * @author Pablo López Torres <pablolopeztorres@gmail.com> 
 */
class Parser
{
	/**
	 * detectStartPhpToken
	 * 
	 * @static
	 * @access public
	 * @author Pablo López Torres <pablolopeztorres@gmail.com> 
	 * @param array $tokens 
	 * @return array|NULL
	 */
	public static function detectStartPhpToken(array &$tokens)
	{
		$token =& current($tokens);
		$subtokens = NULL;
		if ($token[0] === T_OPEN_TAG)
		{
			$subtokens = array(key($tokens) => $token);
			next($tokens);
		}
		return $subtokens;
	}

	public static function detectEndPhpToken(array &$tokens)
	{
		
	}

	/**
	 * Tries to detect if the list of tokens provided starts by a class.
	 * 
	 * Returns the class and advances the internal $tokens pointer to the
     * next token available after the class
	 * 
	 * @static
	 * @access public
	 * @author Pablo López Torres <pablolopeztorres@gmail.com> 
	 * @return PhpClass|NULL PhpClass Class or NULL if not found
	 */
	public static function detectClass(array &$tokens, $autoIndex = FALSE)
	{
		// class config
		$className = NULL;
		$parentName = NULL;
		$interfaces = array();
		$isAbstract = FALSE;
		$isFinal = FALSE;

		$workingTokens = $tokens;
		$token =& current($workingTokens);

		// optional PhpDoc comment
		if ($token[0] === T_DOC_COMMENT)
		{
			$token =& next($workingTokens);
		}

		// detect header of the class
		$appearsToBeAClass = TRUE;
        while ($token && $appearsToBeAClass && ! isset($isAClass))
        {
            if ($token[0] === T_CLASS)
            {
				// class name
				$token =& next($workingTokens);
				$className = $token[1];

				// next element
				$token =& next($workingTokens);

				// inheritance and interfaces
				while ($token[0] !== T_USER_BRACE_OPEN && ! isset($isAClass))
				{
					switch ($token[0])
					{
						case T_EXTENDS:
							// parent name
							$token =& next($workingTokens);
							$parentName = $token[1];

							// next element
							$token =& next($workingTokens);
							break;

						case T_IMPLEMENTS:
							// interface name
							$token =& next($workingTokens);
							$interfaces[] = $token[1];

							// next element
							$token =& next($workingTokens);

							// optional comma and more interfaces
							while ($token[0] === T_USER_COMMA)
							{
								// interface name
								$token =& next($workingTokens);
								$interfaces[] = $token[1];

								// next element
								$token =& next($workingTokens);
							}
							break;
						default:
							$isAClass = FALSE;
							break;
					}
				}

				// open brace
				if ($token[0] === T_USER_BRACE_OPEN)
				{
					$isAClass = TRUE;
				}
				else
				{
					$isAClass = FALSE;
				}
            }
			elseif (in_array($token[0], array(T_ABSTRACT, T_FINAL)))
			{
				switch ($token[0])
				{
                    case T_ABSTRACT:
                        $isAbstract = TRUE;
                        break;
                    case T_FINAL:
                        $isFinal = TRUE;
                        break;
				}
				// do nothing: valid token for the header of the class
				$token =& next($workingTokens);
			}
			else
			{
				var_dump("Unknown: " . key($workingTokens));
				var_dump($token);
				$isAClass = FALSE;
			}

        }

		$class = NULL;
		if ($isAClass) 
		{
			// detect the end of the class
			self::findClosingParenteses($workingTokens);

			// include the closing parentheses into the list of tokens to be saved
			next($workingTokens);
			
			// store all the tokens that belongs to the class
			$elementTokens = array();
			while (current($tokens) !== current($workingTokens))
			{
				$elementTokens[key($tokens)] = current($tokens);
				next($tokens);
			}
			
			// create the class
			$class = new PhpClass($className);
			$class->setAbstract($isAbstract);
			$class->setFinal($isFinal);
			if ($parentName !== NULL)
			{
				$class->setParentName($parentName);
			}
			$class->setInterfaceNames($interfaces);
			$class->setSourceTokens($elementTokens);

			if ($autoIndex)
			{
				$class->index();
			}
		}

		return $class;
	}

	public static function findClosingParenteses(&$tokens)
	{
		// @todo pablo - add all kind of parentheses
		$parentheses = array(
			T_USER_BRACE_OPEN => T_USER_BRACE_CLOSE,
			T_USER_PARENTHESES_OPEN => T_USER_PARENTHESES_CLOSE,
		);

		// open parentheses
		$token =& current($tokens);

		if (! in_array($token[0], array_keys($parentheses)))
		{
			throw new Exception('Current element in $tokens should be a opening parentheses');
		}

		$openBracketSymbol = $token[0];
		$closeBracketSymbol = $parentheses[$openBracketSymbol];

		$indentationLevel = 1;
		while ($indentationLevel > 0)
		{
			$token =& next($tokens);
			if ($token[0] === $closeBracketSymbol)
			{
				$indentationLevel--;
			}
			if ($token[0] === $openBracketSymbol)
			{
				$indentationLevel++;
			}
		}
	}

	/**
	 * detectInterface
	 * 
	 * @static
	 * @access public
	 * @author Pablo López Torres <pablolopeztorres@gmail.com> 
	 * @return array|NULL Tokens that form the class or NULL if not found
	 */
	public static function detectInterface(array &$tokens)
	{

	}


    /**
     * fetchClass
     * 
     * @param array $tokens 
     * @static
     * @author Pablo López Torres <pablolopeztorres@gmail.com> 
     * @access public
     * @return PhpClass
     */
    public static function fetchClass(array &$tokens, $file = NULL, $project = NULL)
    {
        $token =& current($tokens);

        $isAClass = FALSE;
        $element = NULL;

        // get sign
        $sign = array();
        while ($token[0] !== T_USER_BRACE_OPEN)
        {
            if ($token[0] === T_CLASS)
            {
                $isAClass = TRUE;
            }

            $sign[] = $token;

            next($tokens);
            $token =& current($tokens);
        }

        if ($isAClass)
        {
            $element = new PhpClass($file, $project);

            if (! $element->fillFromHeader($sign))
            {
                throw new Exception("wrong source code, please run php -r {$argv[0]}");
            }

            if (! $element->fillFromSource($tokens))
            {
                throw new Exception("wrong source code, please run php -r {$argv[0]}");
            }
        }

        return $element;
    }
    /**
     * fetchInterface 
     * 
     * @param array $tokens 
     * @static
     * @author Pablo López Torres <pablolopeztorres@gmail.com> 
     * @access public
     * @return PhpInterface
     */
    public static function fetchInterface(array &$tokens)
    {
    }
    /**
     * fetchMethod
     * 
     * @param array $tokens 
     * @param mixed $class 
     * @static
     * @author Pablo López Torres <pablolopeztorres@gmail.com> 
     * @access public
     * @return PhpMethod
     */
    public static function fetchMethod(array &$tokens, $class = NULL) {
        // possible combinations
        // T_FUNCTION = (final)? (public|protected|private)? T_FUNCTION [method-name] (

        $token =& current($tokens);

        $isAMethod = FALSE;
        $methodAccess = 'public'; // should we use string for this case?
        $methodName = NULL;
        $methodParams = array();
        $methodContents = array(); // we'll use this to cover all variables inside the method
        $isFinal = FALSE;
        $isAbstract = FALSE;
        $isStatic = FALSE;
		$phpDoc = FALSE;

        // get header
        while ($token[0] !== T_USER_PARENTHESES_OPEN && $token[0] !== T_USER_SEMICOLON)
        {
            if ($token[0] === T_DOC_COMMENT)
            {
                $phpDoc = $token;
				print_r($token);
            }

            if ($token[0] === T_FUNCTION)
            {
                $isAMethod = TRUE;
            }

            $sign[] = $token;

            next($tokens);
            $token =& current($tokens);
        }

        $method = NULL;

        if ($isAMethod)
        {
            reset($sign);
        
            do
            {
                $token =& current($sign);

                switch ($token[0])
                {
                    case T_ABSTRACT:
                        $isAbstract = TRUE;
                        break;
                    case T_FINAL:
                        $isFinal = TRUE;
                        break;
                    case T_STATIC:
                        $isStatic = TRUE;
                        break;
                    case T_PRIVATE:
                        $methodAccess = 'private';
                        break;
                    case T_PROTECTED:
                        $methodAccess = 'protected';
                        break;
                    case T_PUBLIC:
                        $methodAccess = 'public';
                        break;
                    case T_FUNCTION:
                        $token =& next($sign);
                        if ($token[0] === T_USER_AMPERSAND) {
                            // @todo pablo - this should store that is a reference
                            $token =& next($sign);
                        }
                        $methodName = $token[1];
                        break;
                    default:
                        var_dump($token);
                        throw new Exception('unexpected token in method signature!!');
                        break;
                }

            }
            while (next($sign));

            // get parameters
            $params = array();
            $token =& current($tokens);

            // // get the tokens for the parameters
            $level = 0;
            if ($token[0] === T_USER_PARENTHESES_OPEN)
            {
                $level++;
                do
                {
                    next($tokens);
                    $token =& current($tokens);
                    
                    switch ($token[0])
                    {
                        case T_USER_PARENTHESES_OPEN:
                            $level++;
                            break;
                        case T_USER_PARENTHESES_CLOSE:
                            $level--;
                            break;
                    }

                    $params[] = $token;
                }
                while ($level > 0);
            }
            else
            {
                throw new Exception('the pointer of the array should be pointing to the first ( instance');
            }

            // parse parameters
            reset($params);

            // empty param
            $param = array(
                'reference' => FALSE,
                'name' => NULL,
                'type' => NULL,
                'default' => NULL,
            );

            do
            {
                $token =& current($params);

                switch ($token[0])
                {
                    case T_USER_AMPERSAND:
                        $param['reference'] = TRUE;
                        break;
                    case T_ARRAY:
                    case T_STRING:
                        $param['type'] = $token[1];
                        break;
                    case T_VARIABLE:
                        $param['name'] = $token[1];
                        break;
                    case T_USER_ASSIGNMENT:

                        $param['default'] = '';
                        // while not end of the parameter // commutative property is not applicable here ;)
                        while ($token =& next($params) && $token[0] !== T_USER_COMMA)
                        {
                            $param['default'] .= $token[1];
                        }

                        // return to the comma
                        $token =& prev($params);

                        break;
                    case T_USER_COMMA:
                        // store the before param
                        $methodParams[] = array(
                            'reference' => $param['reference'],
                            'name' => $param['name'],
                            'type' => $param['type'],
                            'default' => $param['default'],
                        );
                        // new param
                        $param = array(
                            'reference' => FALSE,
                            'name' => NULL,
                            'type' => NULL,
                            'default' => NULL,
                        );
                        break;
                    case T_USER_PARENTHESES_OPEN:
                    case T_USER_PARENTHESES_CLOSE:
                        // ignore them
                        break;
                    default:
                        var_dump(token_name($token[0]));
                        var_dump($token);
                        throw new Exception('error parsing the parameters section in the method');
                        break;
                }
            }
            while (next($params));

            if (isset($param['name']))
            {
                $methodParams[] = array(
                    'reference' => $param['reference'],
                    'name' => $param['name'],
                    'type' => $param['type'],
                    'default' => $param['default'],
                );
            
                if (! $isAbstract)
                {
                    // get body
                    $token = current($tokens);

                    // parse body
                    $level = 0;
                    if ($token[0] === T_USER_BRACE_OPEN)
                    {
                        do
                        {
                            switch ($token[0])
                            {
                                case T_USER_BRACE_OPEN:
                                    $level++;
                                    break;
                                case T_USER_BRACE_CLOSE:
                                    $level--;
                                    break;
                            }

                            next($tokens);
                            $token =& current($tokens);
                        }
                        while ($level > 0);
                    }
                }
            }

            if ($methodName !== NULL)
            {
                $method = array(
                    'is_abstract' => $isAbstract,
                    'is_final' => $isFinal,
                    'is_static' => $isStatic,
                    'access' => $methodAccess,
                    'name' => $methodName,
                    'params' => $methodParams,
                    'contents' => $methodContents,
                );
            }
        }


        return $method;
    }
    /**
     * fetchVariable 
     * 
     * @param array $tokens 
     * @param mixed $class 
     * @static
     * @author Pablo López Torres <pablolopeztorres@gmail.com> 
     * @access public
     * @return void
     */
    public static function fetchVariable(array &$tokens, $class = NULL)
    {
        // @todo pablo - implement me!!
        return;
        $token =& current($tokens);

        $isAVar = FALSE;
        $varAccess = NULL; // should we use string for this case?
        $varName = NULL;
        $varValue = NULL;
        $isStatic = FALSE;

        // get header
        while ($token[0] !== T_USER_PARENTHESES_OPEN && $token[0] !== T_USER_SEMICOLON)
        {
            if ($token[0] === T_FUNCTION)
            {
                $isAMethod = TRUE;
            }

            $sign[] = $token;

            next($tokens);
            $token =& current($tokens);
        }

        if ($isAMethod)
        {
            reset($sign);
        
            do
            {
                $token =& current($sign);

                switch ($token[0])
                {
                    case T_ABSTRACT:
                        $isAbstract = TRUE;
                        break;
                    case T_FINAL:
                        $isFinal = TRUE;
                        break;
                    case T_STATIC:
                        $isStatic = TRUE;
                        break;
                    case T_PRIVATE:
                        $methodAccess = 'private';
                        break;
                    case T_PROTECTED:
                        $methodAccess = 'protected';
                        break;
                    case T_PUBLIC:
                        $methodAccess = 'public';
                        break;
                    case T_FUNCTION:
                        $token =& next($sign);
                        $methodName = $token[1];
                        break;
                    default:
                        var_dump($token);
                        throw new Exception('unexpected token in method signature!!');
                        break;
                }

            }
            while (next($sign));


            $token =& current($tokens);

            // get parameters
            $params = array();
            while ($token[0] !== T_USER_PARENTHESES_CLOSE)
            {
                $params[] = $token;

                next($tokens);
                $token =& current($tokens);
            }

            // parse parameters
            reset($params);

            // empty param
            $param = array(
                'name' => NULL,
                'type' => NULL,
                'default' => NULL,
            );

            do
            {
                $token =& current($params);

                switch ($token[0])
                {
                    case T_STRING:
                        $param['type'] = $token[1];
                        break;
                    case T_VARIABLE:
                        $param['name'] = $token[1];
                        break;
                    case T_USER_ASSIGNMENT:
                        $token =& next($params);
                        $param['default'] = $token[1];
                        break;
                    case T_USER_COMMA:
                        // store the before param
                        $methodParams[] = array(
                            'name' => $param['name'],
                            'type' => $param['type'],
                            'default' => $param['default'],
                        );
                        // new param
                        $param = array(
                            'name' => NULL,
                            'type' => NULL,
                            'default' => NULL,
                        );
                        break;
                    case T_USER_PARENTHESES_OPEN:
                    case T_USER_PARENTHESES_CLOSE:
                        // ignore them
                        break;
                    default:
                        var_dump(token_name($token[0]));
                        var_dump($token[1]);
                        throw new Exception('error parsing the parameters section in the method');
                        break;
                }
            }
            while (next($params));

            if (isset($param['name']))
            {
                $methodParams[] = array(
                    'name' => $param['name'],
                    'type' => $param['type'],
                    'default' => $param['default'],
                );
            }
            
            if (! $isAbstract)
            {
                // get body
                $token = current($tokens);

                // parse body
                $level = 0;
                if ($token[0] === T_USER_BRACE_OPEN)
                {
                    do
                    {
                        switch ($token[0])
                        {
                            case T_USER_BRACE_OPEN:
                                $level++;
                                break;
                            case T_USER_BRACE_CLOSE:
                                $level--;
                                break;
                        }

                        next($tokens);
                        $token =& current($tokens);
                    }
                    while ($level > 0);
                }
            }
        }

        $method = NULL;
        if ($isAMethod && $methodName !== NULL)
        {
            $method = array(
                'is_abstract' => $isAbstract,
                'is_final' => $isFinal,
                'is_static' => $isStatic,
				'phpdoc' => $phpDoc,
                'access' => $methodAccess,
                'name' => $methodName,
                'params' => $methodParams,
                'contents' => $methodContents,
            );
        }

        return $method;
    }
    /**
     * fetchConstant 
     * 
     * @param array $tokens 
     * @param mixed $class 
     * @static
     * @author Pablo López Torres <pablolopeztorres@gmail.com> 
     * @access public
     * @return void
     */
    public static function fetchConstant(array &$tokens, $class = NULL)
    {
    }


	public static function parseFileFromSource(TokenList &$tokens)
	{
		$token = reset($tokens);
		print_r($tokens);
		$currentContext = self::CONTEXT_HTML;
		$elements = array();
		while ($token = current($tokens))
		{
			switch ($currentContext)
			{
				case self::CONTEXT_HTML:
				case self::CONTEXT_JS:
					if ($phpTokens = self::detectStartPhpToken($tokens))
					{
						$currentContext = self::CONTEXT_PHP;
					}
					break;
				case self::CONTEXT_PHP:
					if ($phpTokens = self::detectEndPhpToken($tokens))
					{
						$currentContext = self::CONTEXT_HTML;
					}
					else
					{
						$elements[] = self::fetchBaseElement($tokens);
					}
					break;
			}
		}

		return $elements;
	}


	const CONTEXT_HTML = 1;
	const CONTEXT_JS = 2;
	const CONTEXT_PHP = 3;

    /**
     * fetchBaseElement
     * 
     * @param array $tokens 
     * @static
     * @author Pablo López Torres <pablolopeztorres@gmail.com> 
     * @access public
     * @return PhpElement
     */
    public static function fetchBaseElement(array &$tokens, $file = NULL, $projectName = NULL)
    {
		if ($element = self::detectClass($tokens))
		{
			// element is a class
		}
		elseif ($element = self::detectInterface($tokens))
		{
			// element is an interface
		}
		else
		{
			throw new Exception('unknown element');
		}
		
        return $element;
    }

}

